テストデータを"上手く"トランザクションでロールバックする方法
背景
テストを実行した後、元のテストデータにリセットしたい。
TRUNCATE して入れ直すのだと時間がかかる。
→ 事前にトランザクションを開始して、最後にロールバックしよう!
問題点:データの可視性
1.事前にトランザクションを開始します。
2.各テストケース用にデータを書き換えます(もちろんコミットはしない)
3.テスト対象のコードから DB に新たに接続します。
2でコミットしてないので、別の接続からは変更前のデータしか見えない!
対処法:MySQL の場合
分離レベルを「READ UNCOMMITTED」にする。
あえてダーティーリードを起こすことで、コミット前のデータを見えるようにします。
テストの並列実行などをしてなければ、基本的には分離レベルを下げても大抵は問題ないです。
問題がでるケースもあるので、頭の隅に置いておいた方が良いと思います。
対処法:PostgreSQL の場合
残念ながら分離レベルを「READ UNCOMMITTED」にしてもダーティーリードが起こらないです。
PostgreSQLでは「READ UNCOMMITTED」は「READ COMMITTED」として扱われてしまいます。
同じスナップショットを使うことで、コミットしてないデータ書き込みが見えます。
スナップショットの仕組みについてはコチラが分かりやすかったです。 スナップショットの使い方についてはコチラに説明と例があります。 リンク先にもありますが分離レベルを「REPEATABLE READ」以上にする必要があります。
対処法:ActiveRecord(Ruby on Rasil)の場合
MySQL の場合
テスト開始/終了に分離レベルのグローバル設定を変更すると楽でした。
code:ruby
ActiveRecord::Base.connection.execute('SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED')
PostgreSQL の場合
BEGIN 前後のタイミングでスナップショットを取得したり設定したりする必要があります。
しかし ActiveRecord だと BEGIN のタイミングが lazy になっているため「BEGIN の直後に何かする」というのが難しいです。
頑張って ActiveRecord のコード漁っていたら lazy を無効化するコードを見つけたので共有します。
code:ruby
# 無効化
ActiveRecord::Base.connection.disable_lazy_transactions!
# 有効化
ActiveRecord::Base.connection.enable_lazy_transactions!
# lazy が有効なのか?
ActiveRecord::Base.connection.transaction_manager.lazy_transactions_enabled?
まとめ
テストデータのリセットでトランザクションのロールバック使う方法は良く聞きますが、結構、落とし穴があります。
落とし穴も多いし、分離レベルを変える場合もあるので、TRUNCATE してデータ入れ直す方が安全だと思います。
筆者のケースではこれをやることで4倍ぐらい速くなりました。
PostgreSQL ならトランザクション単位でテストするので、もうちょっと頑張ればDB1つでテストの並列実行も可能かも
この記事を書き終えて気づいたのですが、論理的な DB を複数用意してテストごとに接続先を変えれば、テストを並列実行できそう。
安全だし速くなりそう……